// 连星WEB版内置AI引擎 by clonne/2021

import "../../lib/ramda.js"
import utils from "../../lib/utils.js"
import port from "./ai-port.js"
import game from "./game.js"
import openings from "./opening-book.js"


const BOARD_LEN = game.BOARD_LEN
const BOARD_SIZE = game.BOARD_SIZE
const P_EMPTY = game.P_EMPTY
const P_BLACK = game.P_BLACK
const P_WHITE = game.P_WHITE
const SKYMETA = game.to_i(...(game.STARS[0]))
const STARS = [
    game.to_i(...(game.STARS[1])),
    game.to_i(...(game.STARS[2])),
    game.to_i(...(game.STARS[3])),
    game.to_i(...(game.STARS[4])),
]
const RESEVER_ABLE_MS = 1200
const VC_DEPTH_MIN = 8
const VC_DEPTH_MAX = 32
const VC_MOVES_MAX_N = 4
const SEARCH_DEPTH_MIN = 4
const SEARCH_DEPTH_MAX = 16
const SEARCH_MOVES_MAX_N = 6

const ZOBRIST_LEN = BOARD_LEN*3
const ZOBRIST = new Uint32Array(ZOBRIST_LEN)
for (let i = 0; i < ZOBRIST_LEN; i++){
    ZOBRIST[i] = Math.floor(Math.random() * 4294967295)
}
function board_hash(_from){
    if (R.is(Int8Array, _from)) {
        let code = 0
        for (let i = 0; i < BOARD_LEN; i++){
            code ^= ZOBRIST[((1+_from[i]) * BOARD_LEN) + i]
        }
        return code
    }
    return _from[1]
}

function board_arr(_from=undefined) {
    if (R.is(Int8Array, _from)) {return _from}
    return _from[0]
}

function board_make(_from=undefined) {
    if (R.is(Int8Array, _from)) {
        return [new Int8Array(_from), board_hash(_from)]
    } else if (R.is(Object, _from)) {
        return [new Int8Array(_from[0]), _from[1]]
    }
    const arr = new Int8Array(BOARD_LEN)
    arr.fill(P_EMPTY)
    return [arr, 0]
}

function board_clear(board) {
    board[0].fill(P_EMPTY)
    board[1] = 0
    return board
}

function board_set(hash_board, loc, p) {
    hash_board[1] ^= ZOBRIST[((1+hash_board[0][loc]) * BOARD_LEN) + loc]
    hash_board[1] ^= ZOBRIST[((1+p) * BOARD_LEN) + loc]
    hash_board[0][loc] = p
    return hash_board
}

function board_get(hash_board, loc) {
    return hash_board[0][loc]
}

function port_p_to_pcur(port_piece){
    if (port_piece.is_black()){return P_BLACK}
    if (port_piece.is_white()){return P_WHITE}
    return P_EMPTY
}
function loc_distance(a, b){
    const dx = Math.abs(game.to_x(a) - game.to_x(b))
    const dy = Math.abs(game.to_y(a) - game.to_y(b))
    return Math.max(dx, dy)
}
function loc_around_pieces(loc, board, max_distance=1){
    const ways = [[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [1, 1], [1, -1], [-1, 1]]
    const ps = []
    for (let i = 1; i <= max_distance; i++){
        for (let way of ways){
            const x = game.to_x(loc) + (way[0] * i)
            const y = game.to_y(loc) + (way[1] * i)
            if ((x >= 0 && x < BOARD_SIZE) && (y >= 0 && y < BOARD_SIZE)){
                const add = game.to_i(x,y)
                ps.push([add, board_get(board, add)])
            }
        }
    }
    return ps
}
function loc_around_piece_num(loc, board, max_distance=1) {
    const ways = [[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [1, 1], [1, -1], [-1, 1]]
    const loc_x = game.to_x(loc)
    const loc_y = game.to_y(loc)
    let n = 0
    for (let i = 1; i <= max_distance; i++){
        for (let way of ways){
            const x = loc_x + (way[0] * i)
            const y = loc_y + (way[1] * i)
            if ((x >= 0 && x < BOARD_SIZE) && (y >= 0 && y < BOARD_SIZE)){
                n += Math.abs(board_get(board, game.to_i(x, y)))
            }
        }
    }
    return n
}

const Format = {
    shapes(shapes){
        const texts = []
        for (let a in shapes){
            const line = [`${a}:`]
            for (let loc of shapes[a]){
                line.push(` ${game.text_i(loc)}`)
            }
            texts.push(line.join(''))
        }
        return texts.join('\n')
    },
    moves(iterable_moves, prefix=""){
        const locs = (R.is(Array, iterable_moves) ? iterable_moves : [...iterable_moves])
        return locs.reduce((pre, a)=>(`${pre}|${game.text_i(a)}`), prefix)
    },
}

class _State {
    constructor(){
        this.use_forbidden = false
        this.can_be_swap = false
        this.vc_depth = VC_DEPTH_MIN
        this.search_depth = SEARCH_DEPTH_MIN
        this.board = board_make()
        this.hashs_valution = new Map()
        this.hashs_moves_stand = new Map()
        this.hashs_moves_vc = new Map()
    }
    gc() {
        this.hashs_valution.clear()
        this.hashs_moves_stand.clear()
        this.hashs_moves_vc.clear()
        return this
    }
}
const State = new _State()

const Out = {
    reply(id, args){
        if (1 == arguments.length){
            self.postMessage(arguments[0].toString())
        }else if (2 == arguments.length){
            self.postMessage(port.reply(id, args))
        }
    },
    report_error(msg){
        self.postMessage(port.reply("ERROR", msg))
    },
    report_say(text){
        self.postMessage(port.reply("SAY", text))
    },
}

// 在局面的一个点检测形成的棋形
const SHAPE_UR = 0
const SHAPE_SSR = 1
const SHAPE_L4 = 2
const SHAPE_S44 = 3
const SHAPE_S4 = 4
const SHAPE_L3 = 5
const SHAPE_S3 = 6
const SHAPE_L2 = 7
const SHAPE_S2 = 8
const SHAPE_L1 = 9
const POINT_SHAPE_TO_SHAPES_ID = {
    [game.UR]:  SHAPE_UR,
    [game.SSR]: SHAPE_SSR,
    [game.L4]:  SHAPE_L4,
    [game.S44]: SHAPE_S44,
    [game.S4]:  SHAPE_S4,
    [game.L3]:  SHAPE_L3,
    [game.S3]:  SHAPE_S3,
    [game.L2]:  SHAPE_L2,
    [game.S2]:  SHAPE_S2,
    [game.L1]:  SHAPE_L1,
}
function make_point_shapes(pcur, board, loc){
    const shapes = new Int8Array(10)
    const lines = game.point(board_arr(board), loc, pcur)
    for (let a of lines){
        const i = POINT_SHAPE_TO_SHAPES_ID[a]
        if (R.is(Number, i)) {shapes[i]++}
    }
    return shapes
}

// 根据一个点的棋形判断禁手
function point_is_forbidden(shapes, pflag){
    if (pflag == P_BLACK){
        if ((shapes[SHAPE_SSR] > 0) ||
            (shapes[SHAPE_S44] > 0) ||
            ((shapes[SHAPE_S4] + shapes[SHAPE_L4]) > 1) ||
            (shapes[SHAPE_L3] > 1)){
            return true
        }
    }
    return false
}

const VALUATION_MAX     = Number.MAX_SAFE_INTEGER
const VALUATION_MIN     = Number.MIN_SAFE_INTEGER
const VALUATION_L4      = 4294967296
const VALUATION_S44     = 65536
const VALUATION_L3      = 4608
const VALUATION_S4      = 4096
const VALUATION_L2      = 256
const VALUATION_S3      = 64
const VALUATION_S2      = 8
// 对一个点位进行评估：该点位loc对于执棋方pflag在局面board中的价值
function valuation_point(pcur, board, loc){
    const shapes = make_point_shapes(pcur, board, loc)
    // 禁手判断
    if (State.use_forbidden && point_is_forbidden(shapes, pcur)){
            return VALUATION_MIN
    }
    // 胜局
    if (shapes[SHAPE_UR] + shapes[SHAPE_SSR] > 0){
        return VALUATION_MAX
    }
    // 常规情形累加棋形分
    const score = (shapes[SHAPE_L4] * VALUATION_L4) +
                    (shapes[SHAPE_S44] * VALUATION_S44) +
                    (shapes[SHAPE_S4] * VALUATION_S4) +
                    (shapes[SHAPE_L3] * VALUATION_L3) +
                    (shapes[SHAPE_S3] * VALUATION_S3) +
                    (shapes[SHAPE_L2] * VALUATION_L2) +
                    (shapes[SHAPE_S2] * VALUATION_S2)
    return score
}
// 对整体局面进行评估：黑棋优 -MAX < 0 > MAX 白棋优
function valuation_whole(board){
    const hash = board_hash(board)
    const cache = State.hashs_valution.get(hash)
    if (! R.isNil(cache)) {return cache}
    let score = 0
    for (let loc = 0; loc < BOARD_LEN; loc++){
        const pcur = board_get(board, loc)
        if (pcur != P_EMPTY){
            let point = valuation_point(pcur, board, loc)
            point = ((pcur == P_BLACK) ? -point : point)
            if ((point <= -VALUATION_L4) || (point >= VALUATION_L4)){
                score = point; break
            }
            score += point
        }
    }
    State.hashs_valution.set(hash, score)
    return score
}
function moves_make_stand(pcur, board){
    const hash = `${1+pcur}${board_hash(board)}`
    const cache = State.hashs_moves_stand.get(hash)
    if (! R.isNil(cache)) {return cache}
    const side = -pcur
    // 根据接下来能形成的棋形划分优先级
    const level = [
        [],     // 0 我方成五/长连点
        [],     // 1 <对方>成五/长连点
        [],     // 2 我方活四点
        [],     // 3 <对方>活四点
        [],     // 4 我方冲四点
        [],     // 5 <对方>冲四点
        [],     // 6 双方活三/活二/眠三点
        [],     // 7 其余棋形
    ]
    for (let loc = 0; loc < BOARD_LEN; loc++){
        if ((board_get(board, loc) == P_EMPTY) && (loc_around_piece_num(loc, board) > 0)){
            const me = make_point_shapes(pcur, board, loc)
            const he = make_point_shapes(side, board, loc)
            if ((me[SHAPE_SSR] + me[SHAPE_UR]) > 0){level[0].push(loc)}
            else if ((he[SHAPE_SSR] + he[SHAPE_UR]) > 0){level[1].push(loc)}
            else if ((me[SHAPE_L4]) > 0){level[2].push(loc)}
            else if ((he[SHAPE_L4]) > 0){level[3].push(loc)}
            else if ((me[SHAPE_S4] + me[SHAPE_S44]) > 0){level[4].push(loc)}
            else if ((he[SHAPE_S4] + he[SHAPE_S44]) > 0){level[5].push(loc)}
            else if ((me[SHAPE_L3] + me[SHAPE_L2] + me[SHAPE_S3] + he[SHAPE_L3] + he[SHAPE_L2] + he[SHAPE_S3]) > 0){level[6].push(loc)}
            else {level[7].push(loc)}
        }
    }
    // 对方已有活三时，将双方的冲四点也纳入思考范围
    if (level[3].length > 0){
        level[3] = level[3].concat(level[4]).concat(level[5])
    // 否则思考双方的冲四/活三/眠三/活二点
    }else{
        level[4] = level[4].concat(level[5]).concat(level[6])
    }
    const val = R.or(level.find((a)=>(a.length>0)), [])
    State.hashs_moves_stand.set(hash, val)
    return val
}

function moves_make_vc(pcur, board){
    const _hash = `${pcur}${board_hash(board)}`
    const cache = State.hashs_moves_vc.get(_hash)
    if (! R.isNil(cache)) {return cache}
    const nexts = [
        [],     // 0 我方成五点
        [],     // 1 <对方>成五点
        [],     // 2 我方活四点
        [],     // 3 <对方>活四点
        [],     // 4 双方冲四点
        [],     // 5 双方活三点
    ]
    for (let loc = 0; loc < BOARD_LEN; loc++){
        if ((board_get(board,loc) == P_EMPTY) && (loc_around_piece_num(loc, board, 2) > 1)){
            const me = make_point_shapes(pcur, board, loc)
            const he = make_point_shapes(-pcur, board, loc)
            if ((me[SHAPE_SSR] + me[SHAPE_UR]) > 0){nexts[0].push(loc)}
            else if ((he[SHAPE_SSR] + he[SHAPE_UR]) > 0){nexts[1].push(loc)}
            else if ((me[SHAPE_L4]) > 0){nexts[2].push(loc)}
            else if ((he[SHAPE_L4]) > 0){nexts[3].push(loc)}
            else if ((he[SHAPE_S4] + he[SHAPE_S44] + me[SHAPE_S4] + me[SHAPE_S44]) > 0){nexts[4].push(loc)}
            else if ((he[SHAPE_L3] + me[SHAPE_L3]) > 0){nexts[5].push(loc)}
        }
    }
    // 对方有活三，则将双方冲四点合并
    if (nexts[3].length > 0){
        nexts[3] = nexts[3].concat(nexts[4])
    // 对方没有活三，则合并双方冲四/活三点
    }else{
        nexts[4] = nexts[4].concat(nexts[5])
    }
    const moves = R.or(nexts.find((a)=>(a.length>0)), [])
    State.hashs_moves_vc.set(_hash, moves)
    return moves
}

function moves_sort_by_value(moves, pcur, board) {
    moves.sort((a,b)=>{
        board_set(board, a, pcur)
        const val_a = valuation_whole(board)
        board_set(board, a, P_EMPTY)
        board_set(board, b, pcur)
        const val_b = valuation_whole(board)
        board_set(board, b, P_EMPTY)
        return Math.abs(val_a) - Math.abs(val_b)
    })
    return moves
}

function moves_random(moves){
    return moves[Math.floor(Math.random() * moves.length)]
}

class Counter {
    constructor() {
        this.reset()
    }
    reset() {
        this.n_node = 0
        this.n_depth = 0
    }
    add_node(n=1) {
        this.n_node += n
        return this
    }
    in_depth(n) {
        this.n_depth = n
        return this
    }
    report() {return this}
}

class CounterNormal extends Counter {
    report() {
        if ((this.n_node % 512) == 0) {
            Out.report_say(`Node [${this.n_depth}]${this.n_node}`)
        }
        return this
    }
}

class CounterVC extends Counter {
    report() {
        if ((this.n_node % 512) == 0) {
            Out.report_say(`VCT/VCF [${this.n_depth}]${this.n_node}`)
        }
        return this
    }
}

class SearchResult {
    constructor(values, min_i, max_i, hit_i) {
        this._values = values
        this._min_i = min_i
        this._max_i = max_i
        this._hit_i = hit_i
    }
    get values() {return this._values}
    get min_i() {return this._min_i}
    get max_i() {return this._max_i}
    get hit_i() {return this._hit_i}
}

function step_search(pcur, board, init_moves, fn_moves_make, moves_max_n, counter, able_ms, max_depth){
    const time_begin = utils.clock.now_ms()
    function search(counter, pcur, board, depth, alpha=Number.MIN_SAFE_INTEGER, beta=Number.MAX_SAFE_INTEGER) {
        counter.in_depth(max_depth - depth).report()
        if (depth > 0 && ((utils.clock.now_ms()-time_begin) < able_ms)) {
            const moves = fn_moves_make(pcur, board)
            if (moves.length > 0) {
                const node_board = board_make(board)
                moves.splice(moves_max_n)
                for (const loc of moves_sort_by_value(moves, pcur, board)) {
                    board_set(node_board, loc, pcur)
                    counter.add_node()
                    const next_value = search(counter, -pcur, node_board, depth-1, alpha, beta)
                    if ((pcur == P_WHITE) && (next_value > alpha)) {
                        alpha = next_value
                    }
                    if ((pcur == P_BLACK) && (next_value < beta)) {
                        beta = next_value
                    }
                    if (alpha >= beta) {break}
                    board_set(node_board, loc, P_EMPTY)
                }

            }
            return (pcur == P_BLACK) ? beta : alpha
        }
        return valuation_whole(board)
    }
    const values = init_moves.map((loc)=>{
        const node_board = board_make(board)
        board_set(node_board, loc, pcur)
        return search(counter, -pcur, node_board, max_depth)
    })
    let min_i = 0
    let max_i = 0
    for (const i in values) {
        if (values[i] > values[max_i]) {
            max_i = i
        } else if (values[i] < values[min_i]) {
            min_i = i
        }
    }
    const hit_i = (pcur == P_BLACK) ? min_i : max_i
    return new SearchResult(values, min_i, max_i, hit_i)
}

function step_normal(pcur, board, able_ms, max_depth) {
    const init_moves = moves_make_stand(pcur, board)
    const sr = step_search(pcur, board, init_moves, moves_make_stand, SEARCH_MOVES_MAX_N, new CounterNormal(), able_ms, max_depth)
    return [init_moves[sr.hit_i]]
}

function step_vc(pcur, board, able_ms, max_depth){
    const init_moves = moves_make_vc(pcur, board)
    const sr = step_search(pcur, board, init_moves, moves_make_vc, VC_MOVES_MAX_N, new CounterVC(), able_ms, max_depth)
    if (Math.abs(sr.values[sr.hit_i]) >= VALUATION_L4) {
        return [init_moves[sr.hit_i]]
    }
    return []
}


function think_step(pcur, board, able_ms){
    const init_moves = moves_make_stand(pcur, board)
    console.log("init-moves:", Format.moves(init_moves))
    if (init_moves.length > 1){
        const time_vc_begin = utils.clock.now_ms()
        const vc = step_vc(pcur, board, Math.floor(able_ms/2), State.vc_depth)
        if (vc.length > 0){
            console.log("vct/vcf-hit", vc)
            return vc
        }
        able_ms -= (utils.clock.now_ms() - time_vc_begin)
        return step_normal(pcur, board, able_ms, State.search_depth)
    }
    return init_moves
}

// 根据棋子情况选择开局
function opening(board, piece_locs, can_be_swap){
    // 第一手：第一手不可被交换时必定天元，否则星位
    if (0 == piece_locs.length){
        return (can_be_swap ? openings.LOCS_1SWAP : [SKYMETA])
    // 第二手：必定紧靠第一手
    }else if (1 == piece_locs.length){
        return loc_around_pieces(piece_locs[0], board).map((a)=>(a[0]))
    // 第三手，选择最内侧棋的范围<=2的空位中离中心最近的至多5个
    }else if (2 == piece_locs.length){
        const pdists = [loc_distance(piece_locs[0], SKYMETA), loc_distance(piece_locs[1], SKYMETA)]
        const ploc = ((pdists[0] > pdists[1]) ? piece_locs[1] : piece_locs[0])
        const sels = []
        for (let i of loc_around_pieces(ploc, board, 2)){
            if (P_EMPTY == i[1]){
                sels.push([i[0], loc_distance(i[0], SKYMETA)])
            }
        }
        sels.sort((a,b)=>(a[1] - b[1])).splice(5)
        return sels.map((a)=>(a[0]))
    }
}

const Take = {}
Take["ai.move"] = (id, port_pflag, can_be_swap, able_second)=>{
    const pcur = port_p_to_pcur(port.Piece(port_pflag))
    if (pcur == P_EMPTY){
        return Out.report_error([id, "P must are (b or w)"])
    }
    State.can_be_swap = ("y" == can_be_swap)
    const piece_locs = []
    for (const loc in board_arr(State.board)) {
        if (board_get(State.board, loc) != P_EMPTY) {
            piece_locs.push(loc)
        }
    }
    // 开局时无须深度思考
    if (piece_locs.length < 3){
        const loc = moves_random(opening(State.board, piece_locs, State.can_be_swap))
        Out.reply(id, port.Location(loc).pack())
    // 只剩一个空位
    }else if ((1 + piece_locs.length) == BOARD_LEN){
        const loc = board_arr(State.board).findIndex((p)=>{return p == P_EMPTY})
        return Out.reply(id, port.Location(loc).pack())
    }else{
        if ((able_second < port.STEP_SECOND_MIN) || (able_second > port.STEP_SECOND_MAX)){
            return Out.report_error([id, `useable-second must in [${port.STEP_SECOND_MIN},${port.STEP_SECOND_MAX}] but give:${able_second}`])
        }
        const moves = think_step(pcur, State.board, (able_second * 1000) - RESEVER_ABLE_MS)
        if (R.isEmpty(moves)){
            Out.reply(id, "pass")
        }else{
            console.log("final-moves:",Format.moves(moves))
            Out.reply(id, port.Location(moves_random(moves)).pack())
        }
    }
}

// 更新棋盘状态，预先缓存局面hash
Take["ai.set_board"] = (_, ...pcodes)=>{
    board_clear(State.board)
    const pieces = []
    port.Board(pcodes).for_pieces((p, x, y)=>{
        const pcur = port_p_to_pcur(p)
        const loc = game.to_i(x, y)
        board_set(State.board, loc, pcur)
        if (pcur != P_EMPTY){pieces.push(loc)}
    })
    State.board_pieces = pieces
    console.log('[', board_hash(State.board), valuation_whole(State.board), ']')
}

const RULE_MAPS = {
    "forbidden": (a)=>{State.use_forbidden = ((a == "on") ? true : false)},
    "!args": (a,b,c)=>{
        const xs = [Number(a), Number(b), Number(c)]
        State.uct_max_time = xs[0]
        State.vc_depth = Math.min(VC_DEPTH_MAX, Math.max(VC_DEPTH_MIN, xs[1]))
        State.search_depth = Math.min(SEARCH_DEPTH_MAX, Math.max(SEARCH_DEPTH_MIN, xs[2]))
    },
}
Take["ai.set_rule"] = (id, ...rule)=>{
    if (rule.length > 1){
        const doing = RULE_MAPS[rule[0]]
        if (R.is(Function, doing)){
            doing(...(rule.slice(1)))
        }else{
            Out.report_error([id, "rule not found", rule])
        }
    }else{
        Out.report_error([id, "not valid rule"])
    }
}

// 选择局面最平衡的一个点
Take["ai.select-a"] = (id, P, ...A)=>{
    const pcur = port_p_to_pcur(port.Piece(P))
    const locs = A.map((a)=>(port.Location(a).i()))
    const best = [0, VALUATION_MAX]
    for (let i in locs){
        const board = board_make(State.board)
        board_set(board, locs[i], pcur)
        const score = Math.abs(valuation_whole(board))
        if (Math.abs(score) < best[1]){
            best[0] = i
            best[1] = score
        }
    }
    Out.reply(id, best[0])
}
// 打点时，随机选择几个距离2以内的点
Take["ai.selects?"] = (id, P, ...N)=>{
    const has_sels = []
    let need_sel_nums = 0
    N.forEach((pack)=>{
        if ("?" == pack){
            need_sel_nums++
        }else{
            has_sels[port.Location(pack).i()] = true
        }
    })
    function can_select(loc_a){
        if (! has_sels[loc_a]){
            for (let loc_b of State.board_pieces){
                if (loc_distance(loc_a, loc_b) < 2){
                    return true
                }
            }
        }
        return false
    }
    const locs = []
    game.board_for_pieces(board_arr(State.board), (pcur, loc)=>{
        if ((pcur == P_EMPTY) && can_select(loc)){
            if (Math.random() < ((need_sel_nums - locs.length) / need_sel_nums)){
                locs.push(loc)
            }
        }
    })
    const loc_packs = locs.slice(0, need_sel_nums).map((a)=>(port.Location(a).pack()))
    Out.reply(id, loc_packs)
}
// 当无禁第一手时，根据先手位置决定交换
// 否则随机
Take["ai.swap?"] = (id)=>{
    let ai_p = port.P_BLACK
    const pieces = board_arr(State.board).filter((pflag)=>(pflag != P_EMPTY))
    if (State.use_forbidden || (State.board_pieces.length > 1)){
        ai_p = (Math.random() < 0.5) ? port.P_BLACK : port.P_WHITE
    }else{
        const loc_diff = loc_distance(State.board_pieces[0], SKYMETA)
        if (loc_diff > 3){ai_p = port.P_WHITE}
    }
    Out.reply(id, ai_p)
}

Take["ai.stop"] = ()=>{
    console.log("(ai stop) ignore")
}

Take["ai.reset"] = ()=>{
    State.gc()
    console.log("(ai reset)")
}

Take["ai.end"] = ()=>{
    console.log("(ai end) ignore")
}


self.onmessage = function(e){
    const code = port.decode(e.data)
    if (code.target){
        Take[code.target](code.id, ...(code.args))
    }
}

